﻿using System;
using System.Collections.Generic;
using Bettzueche.RLLibrary.Functions;
using Bettzueche.RLLibrary.Types;
using Bettzueche.RLLibrary.Util;
using System.Diagnostics;

namespace AMEE1
{
    /// <summary>
    /// Q-Funktion mit Funktionasapproximation nach LinearGradient-Abstiegsmethode und binären Features. Q(s,a)= SUM(F*theta),<para>
    /// Dabei sind theta die Funktionsparameter und F der Merkmalsvektor.</para><para>
    /// Q-Value function (auch action-value function genannt), die einem Zustands-Aktions-Paar einen Wert zuweist.<br />
    /// Q : SxA --> Double
    /// </para><para> // TODO: Class-Beschreibung aktualisieren
    /// Die Dimension des theta-Vektors ergibt sich aus Anzahl Zustände (Partitionen) * Anzahl (diskretisierter) Aktionen.
    /// </para>
    /// <para>
    /// Der Q-Wert eines Zustands s ist die zu erwartende, aufsummierte Belohnung, die ausgehend von s unter Auswahl 
    /// einer bestimmten Aktion a und der anschließenden Verfolgung einer Strategie π erreicht werden kann. Der Q-Wert
    /// ist ein Gütemaß einer Aktion in einem Zustand und seine Funktion wird üblicherweise geschätzt bzw. erlernt.
    /// </para>
    /// </summary>
    class QEFunktion : IEligibilityFunction<Action, State>, IStateValueFunction<State, Action>
    {
        #region Fields
        /// <summary>
        /// Speicher der Erfahrungen. Indiziert mit (AktionsNr,FeatureNr) hält der Speicher das
        /// zugehörige Merkmalsgewicht W und den Lohnberechtigungsfaktor E: WETupel.
        /// </summary>
        /// <remarks>
        /// Speicheroptimierung: Statt je einer Matrix für Gewichte und eligibilities,
        /// lieber eine Dictionary nehmen. Die hat zwar etwas overhead
        /// bezüglich einzelner Elemente und Zugriffszeiten, verbraucht aber viel weniger Platz als eine statische
        /// Matrix mit 6*67Mio. Elementen, von denen die meisten 0 sind.
        /// <para>
        /// Dictionary&lt;AFTupel, WETupel&gt; - Indiziert nach Tupel (AktionsNr,FeatureNr) die Elementtupel
        /// (Weight, Eligibility). Damit hat man auch w und e gemeinsam in einem Objekt.
        /// </para>
        /// </remarks>
        Dictionary<AFTupel, WETupel> _dasHirn;

        /// <summary>γ·λ. Update von <c>TraceDecay</c> oder <c>DiscountFactor</c> aktualisiert diesen Wert.</summary>
        double _updEFactor;

        // Pfad - Ich brauche dringend einen Pfad zur beschleunigung des Programms
        // In Agent.DoStep
        // 1. EFunction.Update(action,state) - hier den Pfad verwalten: add (a,s), remove if.
        // 2. QFunction.Add(eFunc, factor) - Hinweis: Als arg eFunc wird nur dieses Objekt akzeptiert; über Pfad
        TraceSet<AFTupel> _pfad = new TraceSet<AFTupel>();

        readonly Action[] _AktionArr;

        #endregion

        
        #region Constructor(s)

        /// <summary>
        /// Initializes a new instance of the <see cref="QEFunktion"/> class.
        /// </summary>
        /// <param name="actionsCount">Anzahl möglichr Aktionen</param>
        /// <remarks>
        /// Es wird vorausgesetzt, dass für jeden Abschnitt dieselbe Tilings-Organisation für das Unebenheitsmaß gilt und
        /// dass die Anzahl Tiles im (Multi-)Tiling konstant bleibt! Die Tilings-Organisation (Anzahl Tilings) darf sich ändern.
        /// Es wird vorausgesetzt, dass die Kartenkonfiguration sich nicht ändert.
        /// </remarks>
        public QEFunktion(int actionsCount) {
            _AktionArr = new Action[actionsCount];
            for (int a = 0; a < _AktionArr.Length; a++) {
                var action = new Action();
                action.Abschnittsnummer = a;
                _AktionArr[a] = action;
            }
            _dasHirn = new Dictionary<AFTupel, WETupel>();
        }

        #endregion
        
        
        #region IEligibilityFunction<Action, State>

        /// <summary>
        /// NICHT GENUTZT!
        /// Gibt die e-Werte (value) zu den Merkmalen (key), die zum angegebenen (s,a) Tupel gehören.
        /// Setzt den e-Wert für die angegebene Aktion und die spezifizierten Merkmale (Dict-keys) auf den
        /// spezifizierten Wert (Dict-values).
        /// </summary>
        /// <value>
        /// The <see cref="Dictionary{System.Int32, System.Double}"/> mit Merkmals-ID (key) und e-Wert (value).
        /// </value>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        /// <returns>Die e-Werte (value) zu den Merkmalen (key), die zum angegebenen (s,a) Tupel gehören.</returns>
        public Dictionary<int,double> this[Action action, State state] {
            get {
                Dictionary<int, double> result = new Dictionary<int, double>();
                //UmgebungC.SetFeatures(state/*, _config*/); - überflüssig(?)
                var we = new WETupel();
                foreach (int f in state.GetFeatures()) {
                    var af = new AFTupel(action.ID, f);
                    _dasHirn.TryGetValue(af, out we);
                    result[f] = we.E; // = _eMatrix[action.ID, f];
                }
                return result;
            }
            set {
                var we = new WETupel();
                foreach (int f in value.Keys) {
                    var af = new AFTupel(action.ID, f);
                    _dasHirn.TryGetValue(af, out we);
                    we.E = value[f];
                    _dasHirn[af] = we; //_eMatrix[action.ID, f] = value[f];
                }
            }
        }        

        /// <summary>
        /// Wird nicht genutzt!! Immern null!! Intern wird ein anderer TracePath verwendet.
        /// </summary>
        /// <value>
        /// The path.
        /// </value>
        /// <remarks>
        /// Nur für diese (s,a)-Tupel muss q und e aktualisiert werden.
        /// </remarks>
        public TraceSet<SATupel<State, Action>> Path {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets the discount factor (γ 'gamma').
        /// (Used by E-Update method)
        /// </summary>
        /// <value>
        /// The discount factor. Should be between 0 and 1.
        /// </value>
        /// <remarks>
        /// Aktualisiert <c>_updEFactor</c>!
        /// </remarks>
        public double DiscountFactor {
            get { return _gamma; }
            set {
                _gamma = value;
                _updEFactor = _gamma * _lambda;
            }
        }
        double _gamma;

        /// <summary>
        /// Gets or sets the trace decay (λ 'lambda').
        /// <para>(Optional. Eventually used by Update method)</para>
        /// </summary>
        /// <value>
        /// The trace decay.
        /// </value>
        /// <remarks>
        /// Aktualisiert <c>_updEFactor</c>!
        /// </remarks>
        public double TraceDecay {
            get { return _lambda; }
            set {
                _lambda = value;
                _updEFactor = _gamma * _lambda;
            }
        }
        double _lambda;

        /// <summary>
        /// <c>MinE</c> gibt den kleinsten Wert für
        /// 'eligibility' an, der nicht zu 0.0 interpretiert wird. Standard = 0.0.
        /// </summary>
        /// <value>
        /// Der minimale Verantwortlichkeitsfaktor e. Negative Werte werden auf 0.0 gesetzt.
        /// </value>
        public double MinE {
            get { return _minE; }
            set { _minE = Math.Max(0.0, value); }
        }
        double _minE;

        /// <summary>
        /// Updates the eligibility function with respect to given state-action pair.
        /// </summary>
        /// <param name="lastAction">The action.</param>
        /// <param name="lastState">The state.</param>
        /// <returns>This updated <see cref="Bettzueche.RLLibrary.Functions.IEligibilityFunction">IEligibilityFunction&lt;A,S&rt;</see></returns>
        /// <exception cref="System.NullReferenceException">ein oder beide Argumente sind null</exception>
        public IEligibilityFunction<Action, State> Update(Action lastAction, State lastState) {
            if (lastState.FeaturedID == 0) {
                Debug.WriteLine("lastState hat keine Merkmale gesetzt!? Abbruch von QEFunction.Update!", "Warnung");
                return this;
            }
            List<AFTupel> removeList = new List<AFTupel>(_pfad.Count);
            var we = new WETupel();
            // 1. Reduziere alle alten E-Werte: E = γ·λ·E;
            foreach (var af in _pfad) {
                // Daten modifizieren
                _dasHirn.TryGetValue(af, out we);
                we.E = we.E * _updEFactor;
                if (we.E < _minE) {
                    // Minimal-Verantwortlichkeit erreicht: entfernen!
                    we.E = 0.0;
                    removeList.Add(af);
                }
                _dasHirn[af] = we; // Daten aktualisieren
            }
            foreach (var af in removeList) {
                _pfad.Remove(af);
            }

            // 2. Ersetzende Traces
            foreach (int f_s in lastState.GetFeatures()) {
                var af = new AFTupel();
                for (int a = 0; a < _AktionArr.Length; a++) {
                    af = new AFTupel(a, f_s);
                    // 2.a) Setze aktuelle e(F_sa) auf 1 und füge fa dem Pfad hinzu
                    if (lastAction.ID == a) {
                        _dasHirn.TryGetValue(af, out we);
                        we.E = 1.0;
                        _dasHirn[af] = we;
                        _pfad.Add(af);
                    }
                    // 2.b) für alle a ungleich lastAction setze e(F_sa) auf 0 (sofern nötig) und entferne af aus Pfad
                    else {
                        if (_pfad.Contains(af)) {
                            // dieses Merkmal von lastState wurde mit anderer Aktion shconmal genutzt: 
                            // e=0! und af aus Pfad entfernen.
                            _dasHirn.TryGetValue(af, out we);
                            we.E = 0.0;
                            _dasHirn[af] = we;
                            _pfad.Remove(af);
                        }
                    }
                }
            }
            return this;
        }

        /// <summary>
        /// Löscht alle e-Einträge dieser E-Funktion, bzw. setzt sie auf einen Standardwert zurück.
        /// </summary>
        public void Clear() {
            var we = new WETupel();
            foreach (var af in _pfad) {
                _dasHirn.TryGetValue(af, out we);
                we.E = 0;
                _dasHirn[af] = we;
            }
            _pfad.Clear();
        }

        #endregion


        #region IStateValueFunction<State, Action>

        /// <summary>
        /// Gibt den Q-Wert für das angegebene (s,a)-Tupel.
        /// Set nicht implementiert! Benutze stattdessen Add(EFunction, double)!
        /// </summary>
        /// <value>
        /// Not Implemented!!
        /// </value>
        /// <param name="state">The state.</param>
        /// <param name="action">The action.</param>
        /// <returns>Q-Wert für das angegebene (s,a)-Tupel</returns>
        /// <exception cref="System.NotImplementedException"> Set </exception>
        public double this[State state, Action action] {
            get {
                // Q[s,a] = Sum(w_i) | i in F_s
                double result = 0.0;
                var we = new WETupel();
                foreach (int f in state.GetFeatures()) {
                    var af = new AFTupel(action.ID, f);
                    _dasHirn.TryGetValue(af, out we);
                    result += we.W; // weight
                }
                return result;
            }
            set {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Gibt die Bewertungen für alle Aktionen im spezifizierten Zustand, indiziert nach Aktions-ID.
        /// </summary>
        /// <param name="state">The state.</param>
        /// <returns>Die Bewertungen für alle Aktionen im spezifizierten Zustand, indiziert nach Aktions-ID.</returns>
        public double[] GetEstimates(State state) {
            double[] result = new double[_AktionArr.Length];
            for (int a = 0; a < _AktionArr.Length; a++) {
                result[a] = this[state, _AktionArr[a]];
            }
            return result;
        }

        /// <summary>
        /// Adds the specified eligibility values, each one multiplied by 
        /// <c>factor</c>, to this values.
        /// <para>
        /// Conveniance method for updating the Q-Function: W = W + (α·δ)·E or
        /// forall (s,a): Q(s,a) = Q(s,a) + factor * E(a,s)
        /// </para>
        /// </summary>
        /// <param name="eFunction">The e function.</param>
        /// <param name="factor">The factor.</param>
        /// <exception cref="System.NotSupportedException">Als EFunction wird nur diese QEFunction akzeptiert.</exception>
        public void Add(IEligibilityFunction<Action, State> eFunction, double factor) {
            // W = W + α·δ·E
            if (eFunction != this) {
                throw new NotSupportedException("Als EFunction wird nur diese QEFunction akzeptiert.");
            }
            var we = new WETupel();
            foreach (var af in _pfad.Trace) {
                _dasHirn.TryGetValue(af, out we);
                we.W += factor * we.E;
                _dasHirn[af] = we;
            }
        }

        /// <summary>
        /// Äquivalent zu Add(eFunction, 1.0).
        /// </summary>
        /// <param name="eFunction">The e function.</param>
        public void Add(IEligibilityFunction<Action, State> eFunction) {
            this.Add(eFunction, 1.0);
        }
        
        #endregion


        #region Verborgen / nicht implementiert

        /// <summary>
        /// NONSENS! NICHT WIRKLICH UNTERSTÜTZT!<para>
        /// Gibt den Mittelwert der e-Werte zu dem spezifizierten (s,a)-Tupel.
        /// Setzt alle e-Werte für die zum spezifizierten (s,a)-Tupel gehörenden Merkmale auf den
        /// angegebenen Wert.</para>
        /// </summary>
        /// <value>
        /// The <see cref="System.Double"/> value for eligibility.
        /// </value>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        /// <returns>Den Mittelwert der e-Werte zu dem spezifizierten (s,a)-Tupel.</returns>
        double IEligibilityFunction<Action, State>.this[Action action, State state] {
            get {
                // UmgebungC.SetFeatures(state/*, _config*/); - überflüssig(?)
                double sum = 0.0;
                int count = 0;
                var we = new WETupel();
                foreach (int f in state.GetFeatures()) {
                    var af = new AFTupel(action.ID, f);
                    _dasHirn.TryGetValue(af, out we);
                    sum += we.E; // sum += _eMatrix[action.ID, f];
                    count++;
                }
                return sum / (double)count;
            }
            set {
                // UmgebungC.SetFeatures(state/*, _config*/); - überflüssig(?)
                var we = new WETupel();
                foreach (int f in state.GetFeatures()) {
                    var af = new AFTupel(action.ID, f);
                    _dasHirn.TryGetValue(af, out we);
                    we.E = value; // _eMatrix[action.ID, f] = value;
                    _dasHirn[af] = we;
                }
            }
        }
        /// <summary>
        /// Hided member of <see cref="Bettzueche.RLLibrary.Functions.IEligibilityFunction">IEligibilityFunction</see>.
        /// Not implemented!
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        double IEligibilityFunction.this[IAction action, IState state] {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Verborgen und nicht implementiert!
        /// Geerbt von <see cref="Bettzueche.RLLibrary.Functions.IStateValueFunction">IStateValueFunction</see>.
        /// </summary>
        /// <param name="state">The state.</param>
        /// <param name="action">The action.</param>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        double IStateValueFunction.this[IState state, IAction action] {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Nicht implementiert!
        /// </summary>
        /// <param name="m"></param>
        /// <exception cref="System.NotImplementedException"></exception>
        public void Add(Matrix m) {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Verborgen und nicht implementiert!
        /// Geerbt von <see cref="Bettzueche.RLLibrary.Functions.IStateValueFunction">IStateValueFunction</see>.
        /// </summary>
        /// <param name="state">The state.</param>
        /// <param name="action">The action.</param>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        double[] IStateValueFunction.GetEstimates(IState state) {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Verbogene Methode von IEligibilityFunction.
        /// Nicht implementiert!
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        IEligibilityFunction IEligibilityFunction.Update(IAction action, IState state) {
            throw new NotImplementedException();
        }

        #endregion

    }
}
